﻿#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//1.预定义符号-----------------------------------------------------------23
//2.#define 定义常量-----------------------------------------------------41
//3.#define 定义宏-------------------------------------------------------65
//    实现定义一个宏计算一个数的平方-------------------------------------73
//4.带有副作用的宏参数---------------------------------------------------111
//    写一个宏计算两个数的较大值-----------------------------------------119
//5.#和##----------------------------------------------------------------149
//    # 运算符-----------------------------------------------------------150
//    ## 运算符----------------------------------------------------------183
//        使用宏创建函数计算任意同种类型的两个数的较大值-----------------187
//6.宏的命名约定---------------------------------------------------------215
//7.#undef---------------------------------------------------------------221
//8.条件编译-------------------------------------------------------------233
//    单分支的条件编译---------------------------------------------------238
//    多个分支的条件编译-------------------------------------------------256
//    判断是否被定义-----------------------------------------------------278
//9.头文件的包含---------------------------------------------------------309
//    头文件被包含的方式-------------------------------------------------310
//    嵌套文件包含-------------------------------------------------------323

//预定义符号
    //C语言设置了一些预定义符号，可以直接使用，预定义符号也是在预处理期间处理的。
    //1.__FILE__  //进行编译的源文件
    //2.__LINE__  //文件当前的行号 
    //3.__DATE__  //文件被编译的日期
    //4.__TIME__  //文件被编译的时间
    //5.__STDC__  //如果编译器遵循ANSI C，其值为1，否则未定义

//int main()
//{
//    printf("%s\n", __FILE__);
//    printf("%d\n", __LINE__);
//    printf("%s\n", __DATE__);
//    printf("%s\n", __TIME__);
//    //printf("%d\n", __STDC__); //err(未定义)：说明当前的VS2022环境不完全支持ANSI C
//    return 0;
//}

// #define 定义常量
    //基本语法：
    //#define name stuff
//#define MAX 1000
//#define STR "hello world"
//
//int main()
//{
//    int m = MAX;
//    printf("%d\n", m);
//    printf("%s\n", STR);
//    return 0;
//}
    // #define 在预处理阶段就会被清除，将 #define 定义的符号全部转换成对应的内容

    //思考：在define定义标识符的时候，要不要在最后加上 ; --> 建议不要加上 ; ,这样容易导致问题
//#define MAX 1000;
//
//int main()
//{
//    //printf("%d\n", MAX); //err：就像刚刚提到的在预处理阶段 #define 定义的符号会转换成对应的内容
//    return 0;              //     而符号 MAX 的内容为 1000; ，所以会变成printf("%d\n", 1000;); ，这是错误的
//}

// #define 定义宏
    // #define 机制包括了一个规定，允许把参数替换到文本中，这种实现通常称为宏或定义宏
    //下面是宏的申明方式：
    //#define name( parament-list ) stuff
        //其中的 parament-list 是一个由逗号隔开的符号表，它们可能出现在stuff中。
    //需要注意的是：参数列表的左括号必须与name紧邻，如果两者之间有任何空白存在，参数列表就会被解释为stuff的一部分
    //              相当于变成了 #define 定义常量，常量名为name，内容为( parament-list ) stuff，这就和预期不符

//实现定义一个宏计算一个数的平方
//#define SQUARE(x) x*x
//
//int main()
//{
//    int a = 5;
//    int ret = square(a);//和 #define 定义常量一样， #define 定义宏时在预处理阶段就会被清除
//                        //但定义宏会首先将参数替换到文本中，再将宏转变成对应的内容
//                        //也就是说在预处理阶段这个代码会变成：int ret = a*a;
//    printf("%d\n", ret);
//    return 0;
//}

//int main()
//{
//    int a = 5;
//    int ret = SQUARE(a + 1);//由于a的值为5，如果我们想计算6的平方，将括号内的参数改成a+1可以吗？
//                           //通过运行结果我们会发现并不可以，结果为11，这是为什么呢？
//                           //在上面我们知道了：定义宏会首先将参数替换到文本中，再将宏转变成对应的内容
//                           //也就是说首先会把a+1替换x，所以宏对应的内容 x*x 就会替换成 a+1*a+1 
//                           //所以在预处理阶段这个代码会变成：int ret = a+1*a+1;由于没有括号会优先算乘法，故结果为11
//    printf("%d\n", ret);
//    return 0;
//}

    //由上面的代码我们可以知道：定义宏替换到文本中的参数是非常直接的替换，括号里面是什么就替换什么，不会多加少加内容
    //所以我们如果想要计算6的平方，就需要先算加法再算乘法：
//int main()
//{
//    int a = 5;
//    int ret = SQUARE((a + 1));//这样写的话，参数替换到文本中，宏对应的内容 x*x 就会替换成 (a+1)*(a+1) 
//                              //所以在预处理阶段这个代码会变成：int ret = (a+1)*(a+1);运行结果自然就是我们想要的答案
//    printf("%d\n", ret);
//    return 0;
//}
    //友善提示：所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号，
    //          避免在所以宏时由于参数中的操作符或邻近操作符之间的计算优先级顺序导致不可预料的相互作用。

//带有副作用的宏参数
    //当宏参数在宏的定义中出现超过一次的时候，如果参数带有副作用，
    //那么你在使用这个宏的时候就可能出现危险，导致不可预测的后果。
    //副作用就是表达式求值的时候出现的永久性效果。
    // x+1;//不带副作用
    // x++;//带有副作用

    //例如：
//写一个宏，计算两个数的较大值：
//#define MAX(x,y) ((x)>(y)?(x):(y))
//
////int main()
////{
////    int a = 3;
////    int b = 5;
////    int ret = MAX(a, b);
////    printf("%d\n", ret);//5 没有问题
////    return 0;
////}
//
//int main()
//{
//    int a = 3;
//    int b = 5;
//    int ret = MAX(a++, b++);
//    printf("max = %d\n", ret); //max = 6
//    printf("a = %d\n", a);     //a = 4
//    printf("b = %d\n", b);     //b = 7    
//    return 0;
//}
    //这个代码我们本应该认为是a++值为3，b++值为5，和上面一样运行结果仍为5
    //可是为什么和我们想的不一样？这其实也再一次验证了定义宏的原理：
    //如果参数是表达式，宏并不会先将变量代入计算表达式再替换到文本中，
    //而是先将参数直接替换到文本中，将宏转变成对应的内容，最后才是将前面变量的值进行代入
    //所以这里是先将a++和b++替换x和y，则在预处理阶段会变成：int ret = ((a++)>(b++)?(a++):(b++));
    //再将a = 3，b = 5进行代入。首先计算(a++)>(b++)，结果为0，则 : 左边不计算，计算右边表达式
    //此时a = 4，b = 6，则 ret 得到的结果为b++，即为6，而a = 4，b = 7

// #和##
// # 运算符
    //作用：#运算符将宏的一个参数转换为字符串字面量。
    //限制：仅允许出现在带参数的宏的替换列表中。
        //#运算符所执行的操作可以理解为”字符串化“。

    //在应用#运算符之前我们要知道 printf 函数的一个特点：就是 printf 括号内如果有多个双引号内容，是可以直接拼凑在一起的
//int main()
//{
//    printf("hello world\n");
//    printf("hello"" world\n"); //两个双引号之间的空格不会影响打印结果
//    printf("hello" " world""\n"); //三个打印的结果是完全一样的
//    return 0;
//}

//当有一个变量 int a = 10 ; 的时候，我们想用一个宏打印出： the value of a is 10 ，就可以利用#运算符：
//#define Print(n, format) printf("the value of " #n " is " format "\n", n)
//
//int main()
//{
//    int a = 1;
//    Print(a, "%d"); //预处理阶段就会转变成：printf("the value of " "a" " is " "%d" "\n", a)，
//                    //#运算符的作用就是将参数 a 转换成字符串字面量 "a" ，
//                    //由于参数不是字符串，而 printf 函数的内容如果要全面打印出来就必须全是以双引号(即字符串形式)拼凑在一起
//                    //所以对于不是字符串的参数来说，#运算符就起到了作用
//    int b = 20;
//    Print(b, "%d");
//
//    float f = 5.6f;
//    Print(f, "%f");
//
//    return 0;
//}

// ## 运算符
    //作用：## 可以把位于它两边的符号合成一个符号，它允许宏定义从分离的文本片段创建标识符。
    // ## 被称为记号粘合，这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

//使用宏创建函数计算任意同种类型的两个数的较大值；
//#define GENERIC_MAX(type) \
//     type type##_max(type x, type y) \
//    {    \
//        return x>y?x:y; \
//    }                // \ 为续行符，作用是将多行的内容也能当成在一行上处理，适用于需要一行写但内容较多的代码
//                     //原因就是 #define 定义的宏必须是在一行上完成代码
//    //之所以 type 和 _max 之间要加上##运算符，目的是将两个符号当成分离的文本片段而不是整体，
//    //如果不加##，则type_max是默认当成一个整体，此时参数 int 则不会将其中的 type 替代，这就和我们预期不符
//    //而加上##，type 和 _max 则会当成两个分离的符号，此时参数 int 就会替换 type ，
//    //再通过##运算符的作用将位于它两边的符号合成一个符号，即变成了 int_max 这个函数名
//
//GENERIC_MAX(int) //利用宏创建 int_max 函数：int int_max(int x, int y) { return x>y?x:y; }
//GENERIC_MAX(float) //利用宏创建 float_max 函数：float float_max(float x, float y) { return x>y?x:y; }
//
//int main()
//{
//    int a = 3;
//    int b = 5;
//    int ret1 = int_max(a, b);
//    printf("%d\n", ret1);
//    float c = 3.14f;
//    float d = 5.12f;
//    float ret2 = float_max(c, d);
//    printf("%f\n", ret2);
//    return 0;
//}

//宏的命名约定
    //一般来讲函数和宏的使用语法很相似，所以语言本身没法帮我们区分二者。
    //但有一个习惯是：
        //把宏名全部大写
        //函数名不要全部大写

// #undef
    //作用：这条指令用于移除一个宏定义。
//#define MAX 100
//
//int main()
//{
//    printf("%d\n", MAX);
//#undef MAX
//    //printf("%d\n", MAX); //err: 未定义标识符MAX
//    return 0;
//}

//条件编译
    //作用：在编译一个程序的时候可以将一条语句（⼀组语句）编译或者放弃
    //比如说：调试性的代码，删除可惜，保留又碍事，所以我们可以选择性的编译

//常见的条件编译指令：
    //1.单分支的条件编译
    // #if  常量表达式
           //...
    // #endif

//#define M 10
//int main()
//{
//#if M == 10
//    printf("hehe\n");
//#endif
//
//#if M == 0
//    printf("haha\n"); //由于不满足 #if 的条件，所以内部的代码不会被编译
//#endif
//    return 0;
//}

    //2.多个分支的条件编译
    // #if  常量表达式
            //...
    // #elif 常量表达式
            //...
    // #else
            //...
    // #endif

//#define M 2
//int main()
//{
//#if M == 0
//    printf("hehe\n");
//#elif M == 1
//    printf("haha\n");
//#else
//    printf("hello world\n");
//#endif
//    return 0;
//}

    //3.判断是否被定义
    //有两种写法：
    //(1) #if defined(symbol)
    //(2) #ifdef symbol
        //以上两种是判断有定义的时候进行编译，反之不会

    //(1) #if !defined(symbol)
    //(2) #ifndef symbol
        //以上两种是判断无定义的时候进行编译，反之不会

//#define M 0
//int main()
//{
//#if defined M //注意：只会判断是否有定义，对于定义符号对应的值是多少不关心
//    printf("hehe\n");
//#endif
//
//#ifdef M
//    printf("hehe\n");
//#endif
//
//#if !defined M
//    printf("hehe\n");//不会进行编译
//#endif
//
//#ifndef M
//    printf("hehe\n");//不会进行编译
//#endif
//    return 0;
//}

//头文件的包含(更倾向于面试问题，重在理解)
//头文件被包含的方式：
    //1.本地文件包含
        //#include "filename"
    //查找策略：
        //先在源文件所在目录下查找，如果该头文件未找到，编译器就像查找库函数头文件一样在标准位置查找头文件。

    //2.库文件包含
        //#include <filename.h>
    //查找策略：
        //查找头文件直接去标准路径下去查找，如果找不到就提示编译错误。

    //所以 #include <filename.h> 和 #include "filename.h" 两者的区别就在于：查找策略不同

//嵌套文件包含
    //我们知道， #include 指令可以使另外一个文件被编译，就像它实际出现于 #include 指令的地方一样
    //这种替换的方式很简单：预处理器先删除这条指令，并用包含文件的内容替换
    //如果在一个文件当中一个头文件被包含10次，那就实际被编译10次，如果重复包含，对编译的压力就比较大。
    //一个简单直观的例子：
    //#include "Add.h"
    //#include "Add.h"
    //#include "Add.h"
    //#include "Add.h"
    //#include "Add.h"
    //int main()
    //{
    //
    //    return 0;
    //}
    //如果直接这样写，study10.4.c文件中将Add.h包含了5次，那么Add.h文件的内容也将会被拷贝5份在study10.4.c中。
    //如果 test.h 文件比较大，这样预处理后代码量会剧增
    //如果工程比较大，有公共使用的头文件，被大家都能使用，又不做任何的处理，那么后果真的不堪设想

    //条件编译就能解决头文件被重复引入的问题
    //有两种写法：

    //1.在头文件开头写：
        //#ifndef __TEST_H__
        //#define __TEST_H__
        //    //头文件的内容 
        //#endif
    // #ifndef 这个代码刚刚我们已经知道：是判断在无定义的时候进行编译，反之不会
    //所以当第一次进入头文件时由于没有定义 __TEST_H__ ，所以编译下面的代码，也就是定义 __TEST_H__ ，然后包含头文件内容
    //而当后续如果再一次要进入头文件进行包含时，由于已经定义了 __TEST_H__ ，不满足 #ifndef 条件编译，则就不会再次包含头文件
    //这就就可以避免头文件的重复引入

    //2.在头文件开头写：
        //#pragma once
    //其实我们会发生，在VS环境下，创建新的头文件时，第一行默认就会有这条代码
    //这个代码的作用就是避免头文件的重复引入，和上面的意思完全一样，就不需要我们去额外用第一种写法避免头文件的重复引入